<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>原生 HTML + CDN Vue3 虚拟列表</title>
  <style>
    html,body{margin:0;height:100%;font-family:Arial,Helvetica,sans-serif}
    #app{height:100%;display:flex;flex-direction:column;align-items:center}
    h2{margin:12px 0}
    .vlist-wrap{
      width:360px;
      height:500px;
      border:1px solid #ccc;
      overflow:auto;
      position:relative;
    }
    .vlist-phantom{position:absolute;top:0;left:0;right:0;z-index:-1}
    .vlist-row{
      position:absolute;
      left:0;
      right:0;
      height:50px;
      line-height:50px;
      border-bottom:1px solid #eee;
      padding-left:12px;
      box-sizing:border-box;
    }
  </style>
</head>
<body>
  <div id="app">
    <h2>虚拟列表（{{ list.length }} 条数据）</h2>

    <!-- 虚拟列表容器 -->
    <div class="vlist-wrap" ref="scroller" @scroll="onScroll">
      <!-- 撑开滚动条 -->
      <div class="vlist-phantom" :style="{height:totalHeight+'px'}"></div>
      <!-- 可视行 -->
      <div class="vlist-row" v-for="r in visibleRows" :key="r.id"
           :style="{top:r.top+'px'}">
        #{{ r.id }} —— {{ r.txt }}
      </div>
    </div>
  </div>

  <!-- 1. 从 CDN 引入 Vue3 -->
  <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
  <script>
    /* 2. 生成 10 万条假数据 */
    function buildData(len = 100000) {
      return Array.from({ length: len }, (_, i) => ({
        id: i + 1,
        txt: `这是第 ${i + 1} 行内容，随机值 → ${Math.random().toFixed(3)}`
      }));
    }

    /* 3. 创建 Vue 应用 */
    const { createApp, ref, computed, onMounted } = Vue;

    createApp({
      setup() {
        const list = ref(buildData());          // 全量数据
        const scroller = ref(null);             // 容器节点
        const itemHeight = 50;                  // 固定行高（px）
        const buffer = 5;                       // 上下缓冲行数
        const scrollTop = ref(0);               // 实时滚动距离
        const wrapHeight = ref(0);              // 可视区高度

        /* 总高度 = 行数 * 行高 */
        const totalHeight = computed(() => list.value.length * itemHeight);

        /* 计算当前应该渲染哪些行 */
        const visibleRows = computed(() => {
          const start = Math.floor(scrollTop.value / itemHeight);
          const end   = Math.ceil((scrollTop.value + wrapHeight.value) / itemHeight);
          const sliceStart = Math.max(0, start - buffer);
          const sliceEnd   = Math.min(list.value.length, end + buffer);
          return list.value.slice(sliceStart, sliceEnd).map((item, offset) => ({
            ...item,
            top: (sliceStart + offset) * itemHeight
          }));
        });

        /* 滚动回调 */
        function onScroll(e) {
          scrollTop.value = e.target.scrollTop;
        }

        /* 挂载后量一下可视区高度 */
        onMounted(() => {
          wrapHeight.value = scroller.value.clientHeight;
        });

        return { list, scroller, totalHeight, visibleRows, onScroll };
      }
    }).mount('#app');
  </script>
</body>
</html>
